(require :usocket)
(require :yason)
(require :url-rewrite)

(defpackage :cl-tibly
  (:use :common-lisp :usocket :yason :url-rewrite)
  (:export :tibly
	   :tibly-apikey
	   :tibly-login
	   :tibly-shorten
	   :tibly-expand
	   :tibly-info
	   :tibly-stats
	   :tibly-errors
	   :tibly-shorten-data
	   :tibly-shorten-data-shortURL
	   :tibly-shorten-data-hash
	   :tibly-expand-data
	   :tibly-expand-data-longURL
	   :tibly-info-data
	   :tibly-info-data-pairs
	   :tibly-stats-data
	   :tibly-stats-data-clicks))

(in-package :cl-tibly)

;;;; Holds data to establish a connection to Bitly.
(defclass tibly ()
  ((apikey :initarg :apikey
	   :accessor tibly-apikey)
   (login :initarg :login
	  :accessor tibly-login)))

;;;; Returned by /shorten (tibly-shorten).
(defclass tibly-shorten-data ()
  ((shortURL :initarg :shortURL
	     :accessor tibly-shorten-data-shortURL)
   (hash :initarg :hash
	 :accessor tibly-shorten-data-hash)))

;;;; Returned by /expand (tibly-expand)
(defclass tibly-expand-data ()
  ((longURL :initarg :longURL
	    :accessor tibly-expand-data-longURL)))

;;;; Returned by /info (tibly-info)
(defclass tibly-info-data ()
  ((info-hash :initarg :info-hash
	      :accessor tibly-info-data-pairs)))

;;;; Returned by /stats (tibly-stats)
(defclass tibly-stats-data ()
  ((clicks :initarg :clicks
	   :accessor tibly-stats-data-clicks)))

(defgeneric tibly-shorten (tib longURL))
(defgeneric tibly-expand (tib &key shortURL hash))
(defgeneric tibly-info (tib &key shortURL hash keys))
(defgeneric tibly-stats (tib &key shortURL hash))
(defgeneric tibly-errors (tib))

;;;; Returns instance of tibly-shorten-data, or nil.
(defmethod tibly-shorten ((tib tibly) longURL)
  (let ((socket (socket-to-bitly)))
    (format (socket-stream socket) 
	    "~A" 
	    (output-http-headers-shorten tib longURL))
    (force-output (socket-stream socket))
    (setq json-hash (stream-to-json (socket-stream socket)))
    (socket-close socket)
    (setq results (gethash "results" json-hash))
    (when results
      (setq return-object (make-instance 'tibly-shorten-data))
      (setf (tibly-shorten-data-shortURL return-object)
	    (gethash "shortUrl" (gethash longURL results)))
      (setf (tibly-shorten-data-hash return-object)
	    (gethash "hash" (gethash longURL results)))
      (when (and (tibly-shorten-data-shortURL return-object)
		 (tibly-shorten-data-hash return-object))
	return-object))))

;;;; Returns instance of tibly-expand-data, or nil.
(defmethod tibly-expand ((tib tibly) &key shortURL hash)
  (let ((socket (socket-to-bitly)))
    (format (socket-stream socket) 
	    "~A" 
	    (output-http-headers-expand tib :shortURL shortURL :hash hash))
    (force-output (socket-stream socket))
    (setq json-hash (stream-to-json (socket-stream socket)))
    (socket-close socket)
    (setq results (gethash "results" json-hash))
    (when results
      (setq return-object (make-instance 'tibly-expand-data))
      (maphash #'(lambda (key value)
		   (setf 
		    (tibly-expand-data-longURL return-object)
		    (gethash "longUrl" value)))
	       results)
      (when (and (not (hash-table-p (tibly-expand-data-longURL return-object)))
		 (tibly-expand-data-longURL return-object))
	return-object))))
      
	  

;;;; Returns instance of tibly-info-data.
(defmethod tibly-info ((tib tibly) &key shortURL hash keys)
  (let ((socket (socket-to-bitly)))
    (format (socket-stream socket) 
	    "~A" 
	    (output-http-headers-info tib 
				      :shortURL shortURL 
				      :hash hash 
				      :keys keys))
    (force-output (socket-stream socket))
    (setq json-hash (stream-to-json (socket-stream socket)))
    (socket-close socket)
    (setq results (gethash "results" json-hash))
    (when results
      (setq return-object (make-instance 'tibly-info-data
					 :info-hash (make-hash-table)))
      (maphash #'(lambda (key value)
		   (setf (tibly-info-data-pairs return-object) value))
	       results)
      return-object)))

;;;; Returns instance of tibly-stats-data.
(defmethod tibly-stats ((tib tibly) &key shortURL hash)
  (let ((socket (socket-to-bitly)))
    (format (socket-stream socket) 
	    "~A" 
	    (output-http-headers-stats tib :shortURL shortURL :hash hash))
    (force-output (socket-stream socket))
    (setq json-hash (stream-to-json (socket-stream socket)))
    (socket-close socket)
    (setq results (gethash "results" json-hash))
    (when results
      (setq return-object (make-instance 'tibly-stats-data))
      (setf 
       (tibly-stats-data-clicks return-object) 
       (gethash "clicks" results))      
      return-object)))

;;;; Return a list of hash tables. Each contains error information.
(defmethod tibly-errors ((tib tibly))
  (let ((socket (socket-to-bitly)))
    (format (socket-stream socket) 
	    "~A" 
	    (output-http-headers-errors tib))
    (force-output (socket-stream socket))
    (setq json-hash (stream-to-json (socket-stream socket)))
    (socket-close socket)
    (gethash "results" json-hash)))

;;;; HTTP headers for the /shorten request.
(defun output-http-headers-shorten (tib longURL)
  (format nil "~A~A~A~A~A~A ~A~C~C~A~C~C~A~C~C~C~C" 
	  "GET /shorten?version=2.0.1&longUrl=" 
	  (url-encode longURL)
	  "&login=" 
	  (tibly-login tib)
	  "&apiKey=" 
	  (tibly-apikey tib)
	  "HTTP/1.1" #\Return #\Newline
	  "Host: api.bit.ly" #\Return #\Newline
	  "Connection: close" #\Return #\Newline #\Return #\Newline))

;;;; HTTP headers for the /expand request.
(defun output-http-headers-expand (tib &key shortURL hash)
  (format nil "~A~A~A~A~A~A ~A~C~C~A~C~C~A~C~C~C~C" 
	  "GET /expand?version=2.0.1&"
	  (if shortURL 
	      (concatenate 'string "shortUrl=" (url-encode shortURL))
	      (concatenate 'string "hash=" hash))
	  "&login=" 
	  (tibly-login tib)
	  "&apiKey=" 
	  (tibly-apikey tib)
	  "HTTP/1.1" #\Return #\Newline
	  "Host: api.bit.ly" #\Return #\Newline
	  "Connection: close" #\Return #\Newline #\Return #\Newline))

;;;; HTTP headers for the /info request.
(defun output-http-headers-info (tib &key shortURL hash keys)
  (format nil "~A~A~A~A~A~A~A ~A~C~C~A~C~C~A~C~C~C~C" 
	  "GET /info?version=2.0.1&"
	  (if shortURL 
	      (concatenate 'string "shortUrl=" (url-encode shortURL))
	      (concatenate 'string "hash=" hash))
	  "&login=" 
	  (tibly-login tib)
	  "&apiKey=" 
	  (tibly-apikey tib)
	  (if (> (length keys) 0)
	      (format nil "&keys=~{~A~^,~}" keys)
	      "")
	  "HTTP/1.1" #\Return #\Newline
	  "Host: api.bit.ly" #\Return #\Newline
	  "Connection: close" #\Return #\Newline #\Return #\Newline))

;;;; HTTP headers for the /stats request.
(defun output-http-headers-stats (tib &key shortURL hash)
  (format nil "~A~A~A~A~A~A ~A~C~C~A~C~C~A~C~C~C~C" 
	  "GET /stats?version=2.0.1&"
	  (if shortURL 
	      (concatenate 'string "shortUrl=" (url-encode shortURL))
	      (concatenate 'string "hash=" hash))
	  "&login=" 
	  (tibly-login tib)
	  "&apiKey=" 
	  (tibly-apikey tib)
	  "HTTP/1.1" #\Return #\Newline
	  "Host: api.bit.ly" #\Return #\Newline
	  "Connection: close" #\Return #\Newline #\Return #\Newline))

;;;; HTTP headers for the /errors request.
(defun output-http-headers-errors (tib)
  (format nil "~A~A~A~A~A ~A~C~C~A~C~C~A~C~C~C~C" 
	  "GET /errors?version=2.0.1&"
	  "&login=" 
	  (tibly-login tib)
	  "&apiKey=" 
	  (tibly-apikey tib)
	  "HTTP/1.1" #\Return #\Newline
	  "Host: api.bit.ly" #\Return #\Newline
	  "Connection: close" #\Return #\Newline #\Return #\Newline))

(defun socket-to-bitly ()
  (let ((socket (socket-connect "api.bit.ly" 80)))
    socket))

;;;; Reads data until EOF and parses the JSON data.
(defun stream-to-json (stream)
  (let (buffer)
    (do ((line (read-line stream nil) (read-line stream nil)))
	((not line) (parse (data-from-http-response buffer)))
      (setf buffer (concatenate 'string buffer line)))))

;;;; Split the data portion from the HTTP headers portion, and
;;;; return the data.
(defun data-from-http-response (response)
  ;;; HTTP response will be split by using "\r\r".
  ;;; Remember that "\n" was removed by read-line.
  (let* ((split-str (format nil "~C~C" #\Return #\Return))
	 (split-index (search split-str response)))
    (if split-index
	(subseq response (+ split-index 2))
	nil)))